查看原文
其他

古灵精怪的python——地址,浅拷贝与身份运算符

DeepWeaver Python爱好者社区 2019-04-07

作者:DeepWeaver  Python爱好者社区专栏作者

知乎专栏:深度学堂

https://zhuanlan.zhihu.com/c_172487736


首先抛出一个问题,吸引读者的阅读兴趣(如果您觉得这个不是问题,那么这篇文章不适合您:)


请看如下代码:

>>> a = 3 >>> b = 3 >>> a == b True >>> a is b True # 这没问题 >>> a = 3 >>> b = a >>> a == b True >>> a is b True # 这看起来也很合理 >>> a = (2,3) >>> b = (2,3) >>> a is b False # ???why? >>> a == b True >>> b = a >>> a is b True # ???why? >>> a == b True

好了,整篇文章都是围绕这个问题展开的。长久以来我都习惯用is而不用==来进行两个对象的比较(python中一切皆对象) 直到今天出了一个bug后才了解到这两者之间的不同,挖到python的一个大坑之余,不禁出了一身冷汗。。。


is还是==


补充知识


id() 用于获取对象在内存中的地址,并以十进制展示出来。如:

>>> a = 3 >>> id(a) 140602638349720 >>> hex(id(a)) # 还原成我们看着更顺眼的16进制,但是本文以10进制地址为主(因为懒) '0x7fe09a503598'

顾名思义,is是“相同”,而==是指两者之间的”相等“关系。所谓相同,比较的是两者之间的在内存中的位置,

>>> a = 3 >>> id(a) 140602638349720 >>> b = 3 # b指向的是和a指向的同一块地址(但是并不意味这改变了a,b也会相应改变) >>> id(b) 140602638349720 >>> c = a # a的引用复制给c,在内存中其实是指向了用一个对象 >>> id(c) 140602638349720 >>> a is b True >>> a is c True >>> b is c True

我们看到,上面a,b,c的地址相同,所以他们互相之间”相同“

而相等则两者之间的数值对应相等

>>> a = 3 >>> b = a >>> a = 4 >>> b 3 >>> a = [3] >>> b = [3] >>> id(a) 4351374184 >>> id(b) 4351374112 >>> a is b False >>> a == b True >>> a[0] = 4 >>> b [3] >>> a = [3] >>> b = a  # b就是a的引用,占得是同一块地址,而且当a的内容改变时,b也会随之改变,这和上面 # int对象不同,我也不知道为啥要这么搞。 >>> a[0] = 4 >>> b [4]

很多同学看到这肯定是一锅浆糊了,其实就是一个原则,能用==就不用is。除了一种情况,那就是判断对象是否是None。

>>> if a is None: ...     pass

浅拷贝和深拷贝

>>> a = [3] >>> b = a[:] #通过切片赋值,返回的是a的浅拷贝 >>> id(a) 4351273944 >>> id(b) 4351374184 >>> id(a[0]) 140602638349720 >>> id(b[0]) #list的地址不同 140602638349720 >>> a is b False >>> a[0] is b[0] #浅拷贝,只拷贝了a的壳[],里边的内容仍然是同一个东西,同样的id True >>> a == b True >>> a[0] == b[0] True >>> a[0] = 4 # 但是,b的内容不会随着a的变化而变化 >>> b [3]

浅拷贝拷贝了最外层容器,副本中的元素是原容器中元素的引用

我们再看一个例子

>>> Anndy = ['Anndy', ['age', 24]] >>> Tom = Anndy[:] >>> Cindy = list(Anndy) >>> id(Anndy) 4351374040 >>> id(Tom) 4351373968 >>> id(Cindy) 4351374616 >>> print(Anndy, Tom, Cindy) (['Anndy', ['age', 24]],['Anndy', ['age', 24]],['Anndy', ['age', 24]]) # 看起来是创建了三个不同的对象,因为他们的id各不相同 >>> Tom[0] = 'Tom' >>> Cindy[0] = 'Cindy' >>> print (Anndy, Tom, Cindy) (['Anndy', ['age', 24]], ['Tom', ['age', 24]], ['Cindy', ['age', 24]]) # 如果想修改某一个人的名字也没有什么问题 # 现在我们想把Tom的年龄修改为12岁 >>> Tom[1][1] = 12 >>> print (Anndy, Tom, Cindy) (['Anndy', ['age', 12]], ['Tom', ['age', 12]], ['Cindy', ['age', 12]]) # 震惊!所有人的年龄都变成了12!!! >>> print ([id(x) for x in Anndy]) [4351366368, 4351374112] # 看第二个列表的地址 >>> print ([id(x) for x in Tom]) [4351323592, 4351374112] # 看第二个! >>> print ([id(x) for x in Cindy]) [4351366224, 4351374112] # 第一个姓名元素的地址不同,但是第二个列表是同一个

构造方法或切片 [:] 做的是浅拷贝。如果所有元素都是不可变的(比如名字字符串,修改的时候会重新创建对象,仅仅包括原子对象的元组也属于这种情况),那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题,正如刚刚,修改一个人的年龄,所有人的年龄都发生了变化。

所以,如果你想要深拷贝,应该这么写

>>> import copy >>> Anndy = ['Anndy', ['age', 24]] >>> Tom = copy.deepcopy(Anndy) >>> Tom[1][1] = 12 >>> print(Tom, Anndy) (['Anndy', ['age', 12]], ['Anndy', ['age', 24]]) #这样写就没问题了

另外


不知道刚才你有没有注意到

>>> a = 3 >>> b = 3 >>> id(a) 140602638349720 >>> id(b) 140602638349720 # 相同! >>> a is b True

Python会对比较小的整数对象进行缓存缓存起来。当整数比较大的时候就会重新开辟一块内存。

>>> a = 999 >>> b = 999 >>> id(a) 140602638469952 >>> id(b) 140602638469904 # 不同! >>> a is b False

这仅仅是在命令行中执行,而在保存为文件执行,结果是不一样的,这是因为解释器做了一部分优化。

#!/usr/bin/env python a = 3 b = 3 print(a is b) a = 99999 b = 99999 print(a is b) 结果: True True [Finished in 0.0s]

这也是为什么我屡屡用is而不用==,程序运行良好的原因。


总结


 1. python中,尽量不要用is, 除非判断对象是否为None

2. a is b(相同)一定意味着a == b(相等),而a == b(相等) 不一定 a is b (相同)这点比较好理解

3. 如果函数中传参等,需要引用、拷贝的,注意是否生成了一个新的对象,即使生成了,内部元素是否是同一个对象的引用?尤其注意切片的使用。必要的时候用copy模块进行深拷贝而不要用切片这种浅拷贝形式。

Python爱好者社区历史文章大合集

Python爱好者社区历史文章列表(每周append更新一次)

福利:文末扫码立刻关注公众号,“Python爱好者社区”,开始学习Python课程:

关注后在公众号内回复“课程”即可获取:

小编的Python入门免费视频课程!!!

【最新免费微课】小编的Python快速上手matplotlib可视化库!!!

崔老师爬虫实战案例免费学习视频。

陈老师数据分析报告制作免费学习视频。

玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存